package com.xlongwei.accounts.config;

import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.io.OutputStreamWriter;
import java.io.StringReader;
import java.io.Writer;
import java.nio.charset.StandardCharsets;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.Statement;
import java.util.HashMap;
import java.util.Map;
import java.util.Properties;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import javax.sql.DataSource;

import org.h2.tools.RunScript;
import org.noear.snack.ONode;
import org.noear.snack.annotation.ONodeAttr;
import org.noear.snack.core.Feature;
import org.noear.snack.core.Options;
import org.noear.solon.Solon;
import org.noear.solon.Utils;
import org.noear.solon.annotation.Component;
import org.noear.solon.annotation.Destroy;
import org.noear.solon.annotation.Init;
import org.noear.solon.annotation.Inject;
import org.noear.solon.core.AppContext;
import org.noear.solon.core.BeanWrap;
import org.noear.solon.core.util.IoUtil;
import org.noear.solon.data.datasource.DsUtils;

import com.zaxxer.hikari.HikariDataSource;

import lombok.Data;
import lombok.extern.slf4j.Slf4j;

@Data
@Slf4j
@Component
public class DsConfig {
    public static DsConfig INSTANCE;
    public static Map<String, Boolean> supportEmoji = new ConcurrentHashMap<>();
    public static Map<String, CountDownLatch> countdownMap = new ConcurrentHashMap<>();
    private static String dsConfig = "files/ds.json";
    private static String getDsConfig = "getDsConfig", addDsConfig = "addDsConfig", updateDsConfig = "updateDsConfig";
    @Inject
    @ONodeAttr(serialize = false)
    com.xlongwei.accounts.config.AppConfig.DsCacheService cacheService;
    private Map<String, Ds> h2;

    @Init
    public void init() {
        DsConfig.INSTANCE = this;
        Map<String, DataSource> map = Solon.context().getBeansMapOfType(DataSource.class);
        map.entrySet().forEach(entry -> {
            try (Connection conn = entry.getValue().getConnection()) {
                supportEmoji(entry.getKey(), conn);
            } catch (Exception e) {
                log.info("emoji fail={}", e.getMessage());
            }
        });
        if(AppConfig.enableH2 && AppConfig.enableRedis) subscribe();
        if (!AppConfig.isClient) {
        try (InputStream in = new FileInputStream(dsConfig)) {
            String str = IoUtil.transferToString(in);
            ONode.loadStr(str).bindTo(this);
            if (h2 != null) {
                log.info("dsNames={}", h2.keySet());
                h2.forEach((mobile, ds) -> {
                    init(mobile, ds);
                });
            }
        } catch (FileNotFoundException fnfe) {
            log.info("DsConfig file not exist: {}", dsConfig);
        } catch (Exception e) {
            log.warn("fail to load DsConfig from {}", dsConfig, e);
        }
        }else {
            sleep(100);
        }
        if (h2 == null) {
            h2 = new HashMap<>();
        }
        if(AppConfig.isClient) getDsConfig();
        if(AppConfig.isServer) updateDsConfig();
    }

    public boolean add(String mobile, String partner, String pass) {
        Ds ds = h2.get(mobile);
        if (ds != null) {
            return false;
        }
        if (AppConfig.isClient) {
            RedisConfig.bus.publish(AppConfig.channel, new ONode().set("type", addDsConfig)
                    .set("mobile", mobile).set("partner", partner).set("pass", pass).toJson());
            CountDownLatch latch = new CountDownLatch(1);
            countdownMap.put(mobile, latch);
            await(latch, 1500);
            return h2.containsKey(mobile);
        }
        if(!AppConfig.enableH2) return false;
        ds = new Ds();
        ds.setPartner(partner);
        ds.setPass(pass);
        h2.put(mobile, ds);
        init(mobile, ds);
        if(AppConfig.isServer) updateDsConfig();
        save();
        return true;
    }

    public static String ds(String mobile) {
        if (!Utils.isBlank(mobile) && DsConfig.INSTANCE != null) {
            for (Map.Entry<String, Ds> entry : DsConfig.INSTANCE.h2.entrySet()) {
                if (entry.getKey().equals(mobile) || entry.getValue().getPartner().equals(mobile)) {
                    return entry.getKey();
                }
            }
        }
        return "";
    }

    public static boolean supportEmoji(String dsName) {
        Boolean b = supportEmoji.get(Utils.isNotBlank(dsName) ? dsName : "accounts");
        return b == null ? true : b;
    }

    @Destroy
    public void destroy() {
        // save();
    }

    private void init(String mobile, Ds ds) {
        Properties props = new Properties();
        if (AppConfig.isClient) {
            props.put("jdbcUrl", Consts.jdbcTcp.replaceFirst("localhost:28085", AppConfig.serverHostAndPort)
                    .replaceFirst("accounts", mobile));
        } else {
            props.put("jdbcUrl", Consts.jdbcUrl.replaceFirst("accounts", mobile)
                    .replaceFirst("AUTO_SERVER=TRUE", "AUTO_SERVER=" + AppConfig.enableAutoServer));
        }
        // log.info("jdbcUrl={}", props.get("jdbcUrl"));
        props.put("maximumPoolSize", "1");
        props.put("username", "accounts");
        props.put("password", ds.getPass());
        // 参考org.noear.solon.data.integration.XPluginImpl，注册ds
        DataSource dataSource = DsUtils.buildDs(props, HikariDataSource.class);
        // 初始化数据库
        try (Connection conn = dataSource.getConnection()) {
            try (Statement stmt = conn.createStatement()) {
                stmt.execute("SET SCHEMA accounts");
                stmt.executeQuery("select memberid from t_member limit 1");
                log.info("ds={} is ok", mobile);
                supportEmoji(mobile, conn);
            } catch (Exception e) {
                String init = "runscript from 'classpath:db.sql'";
                log.info("ds={} {}", mobile, init);
                RunScript.execute(conn, new StringReader(init));
                // 创建mobile和partner账户
                try (PreparedStatement ps = conn.prepareStatement(Consts.insertUser)) {
                    int idx = 1;
                    ps.setLong(idx++, Long.parseLong(mobile));
                    ps.setString(idx++, ds.getPass());
                    ps.setInt(idx++, 2);
                    ps.setLong(idx++, Long.parseLong(ds.getPartner()));
                    ps.execute();
                    ps.setLong(1, Long.parseLong(ds.getPartner()));
                    ps.setLong(4, Long.parseLong(mobile));
                    ps.execute();
                    log.info("ds={} partner={}", mobile, ds.getPartner());
                }
                supportEmoji(mobile, conn);
            }
            AppContext context = Solon.context();
            BeanWrap dsBw = context.wrap(mobile, dataSource, false);
            context.putWrap(mobile, dsBw);
            context.beanPublish(dsBw);
            cacheService.add(mobile);// 缓存隔离
        } catch (Exception e) {
            log.warn("ds={} fail to init: {}", mobile, e.getMessage());
            throw new RuntimeException(e);
        }
    }

    private void supportEmoji(String dsName, Connection conn) {
        try (Statement stmt = conn.createStatement()) {
            stmt.execute("insert into t_extra(recordid,remark) values(0,'🍒')");
            log.debug("emoji insert={}", stmt.getUpdateCount());
            ResultSet rs = stmt.executeQuery("select remark from t_extra where recordid=0");
            String remark = rs.next() ? rs.getString("remark") : null;
            log.debug("emoji select={}", remark);
            supportEmoji.put(dsName, "🍒".equals(remark));
            rs.close();
            stmt.execute("delete from t_extra where recordid=0");
            log.debug("emoji delete={}", stmt.getUpdateCount());
        } catch (Exception e) {
            log.info("emoji fail={}", e.getMessage());
            supportEmoji.put(dsName, Boolean.FALSE);
        }
        log.info("ds={} emoji={}", dsName, supportEmoji.get(dsName));
    }

    private void save() {
        String str = ONode.loadObj(this, Options.def().add(Feature.PrettyFormat)).toJson();
        try (Writer writer = new OutputStreamWriter(new FileOutputStream(dsConfig), StandardCharsets.UTF_8)) {
            writer.write(str);
        } catch (Exception e) {
            log.warn("fail to write DsConfig to {}", dsConfig, e);
        }
    }

    private void subscribe() {
        RedisConfig.bus.subscribeFuture((channel, message) -> {
            log.info("{}", message);
            ONode node = ONode.loadStr(message);
            String type = node.get("type").getString();
            if(getDsConfig.equals(type) && AppConfig.isServer) {
                updateDsConfig();
            }else if(updateDsConfig.equals(type)) {
                String str = node.get("data").getString();
                Map<String, Ds> bak = new HashMap<>(h2);
                ONode.loadStr(str).bindTo(this);
                if(h2 == null) {
                    h2 = new HashMap<>();
                }else{
                    h2.forEach((mobile, ds) -> {
                        if(!bak.containsKey(mobile)) {
                            log.info("init ds={}", mobile);
                            init(mobile, ds);
                            CountDownLatch latch = countdownMap.remove(mobile);
                            if(latch != null) latch.countDown();
                        }
                    });
                }
            }else if(addDsConfig.equals(type) && AppConfig.isServer) {
                String mobile = node.get("mobile").getString();
                String partner = node.get("partner").getString();
                String pass = node.get("pass").getString();
                boolean add = add(mobile, partner, pass);
                log.info("add={} mobile={} partner={} pass={}", add, mobile, partner, pass);
            }
        }, AppConfig.channel);
        log.info("subscribe...");
    }

    private void getDsConfig() {
        RedisConfig.bus.publish(AppConfig.channel, new ONode().set("type", getDsConfig).toJson());
    }

    private void updateDsConfig() {
        String str = ONode.loadObj(DsConfig.INSTANCE, Options.def()).toJson();
        str = new ONode().set("type", updateDsConfig).set("data", ONode.loadStr(str)).toJson();
        RedisConfig.bus.publish(AppConfig.channel, str);
    }

    private void sleep(long millis) {
        try {
            Thread.sleep(millis);
        } catch (Exception e) {
            log.warn("fail to sleep {} millis, ex: {}", millis, e.getMessage());
        }
    }

    private void await(CountDownLatch latch, long millis) {
        try {
            latch.await(millis, TimeUnit.MILLISECONDS);
        } catch (Exception e) {
            log.warn("fail to wait CountDownLatch {} millis, ex: {}", millis, e.getMessage());
        }
    }

    @Data
    public static class Ds {
        private String partner, pass;
    }

    static interface Consts {
        String insertUser = "insert into t_user(mobile,pass,level,partner) values(?,?,?,?)";
        String jdbcUrl = "jdbc:h2:file:./files/accounts;TRACE_LEVEL_FILE=0;MODE=MYSQL;DATABASE_TO_LOWER=TRUE;AUTO_SERVER=TRUE;IGNORECASE=TRUE;CASE_INSENSITIVE_IDENTIFIERS=TRUE;INIT=CREATE SCHEMA IF NOT EXISTS accounts\\;SET SCHEMA accounts";
        String jdbcTcp = "jdbc:h2:tcp://localhost:28085/file:./files/accounts;TRACE_LEVEL_FILE=0;MODE=MYSQL;DATABASE_TO_LOWER=TRUE;IGNORECASE=TRUE;CASE_INSENSITIVE_IDENTIFIERS=TRUE;INIT=CREATE SCHEMA IF NOT EXISTS accounts\\;SET SCHEMA accounts";
    }
}
